//-----------------------------------------------------------------------------
// Filename: SIPTransaction.cs
//
// Description: SIP Transaction.
// 
// History:
// 14 Feb 2006	Aaron Clauson	Created.
//
// License: 
// This software is licensed under the BSD License http://www.opensource.org/licenses/bsd-license.php
//
// Copyright (c) 2006 Aaron Clauson (aaronc@blueface.ie), Blue Face Ltd, Dublin, Ireland (www.blueface.ie)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that 
// the following conditions are met:
//
// Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 
// Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 
// disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of Blue Face Ltd. 
// nor the names of its contributors may be used to endorse or promote products derived from this software without specific 
// prior written permission. 
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, 
// BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
// IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 
// OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 
// OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
// POSSIBILITY OF SUCH DAMAGE.
//-----------------------------------------------------------------------------

using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Runtime.Serialization;
using System.Text.RegularExpressions;
using BlueFace.VoIP.Authentication;
using CN.VoIP.Net;
using CN.Sys;
using log4net;

#if UNITTEST
using NUnit.Framework;
#endif

namespace CN.VoIP.Net.SIP
{
    public delegate void SIPTransactionStateChange(SIPTransaction sipTransaction);
    public delegate void SIPTransactionAuthenticationRequired(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, SIPTransaction sipTransaction, SIPAuthenticationHeader reqAuthHeader);
    public delegate void SIPTransactionResponseReceived(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, SIPTransaction sipTransaction, SIPResponse sipResponse);
    public delegate void SIPTransactionCancelReceived(SIPTransaction sipTransaction);
    public delegate void SIPTransactionTimedOut(SIPTransaction sipTransaction);
    public delegate void SIPTransactionInviteRequestReceived(UASInviteTransaction uasInviteTransaction);
    public delegate void SIPTransactionRequestRetransmit(SIPTransaction sipTransaction, SIPRequest sipRequest, int retransmitNumber);
    public delegate void SIPTransactionResponseRetransmit(SIPTransaction sipTransaction, SIPResponse sipResponse, int retransmitNumber);

    public enum SIPTransactionStatesEnum
    {
        Unknown = 0,
        Calling = 1,
        Completed = 2,
        Confirmed = 3,
        Proceeding = 4,
        Terminated = 5,
        Trying = 6,
    }

    public enum SIPTransactionTypesEnum
    {
        Unknown = 0,
        Invite = 1,
        NonInvite = 2,
    }
    
    /// <summary>
	/// A response matches a client transaction under two conditions:
	///
	/// 1.  If the response has the same value of the branch parameter in
	/// the top Via header field as the branch parameter in the top
	/// Via header field of the request that created the transaction.
    ///
	/// 2.  If the method parameter in the CSeq header field matches the
	/// method of the request that created the transaction.  The
	/// method is needed since a CANCEL request constitutes a
	/// different transaction, but shares the same value of the branch
	/// parameter.
	/// </summary>
    [DataContract]
	public class SIPTransaction
	{
        public const int TRANSACTION_DURATION = 64; // How long a transaction is maintained for, multiplier of T1.

        protected static ILog logger = AppSpecificState.logger;

        protected static readonly int m_t1 = SIPTimings.T1;       // SIP Timer T1 in milliseconds.

        public int Retransmits = 1;
        public DateTime InitialTransmit = DateTime.MinValue;
        public DateTime LastTransmit = DateTime.MinValue;
        public bool DeliveryPending;
        public bool DeliveryFailed = false;              // If the transport layer does not receive a response to the request in the alloted time the request will be marked as failed.

        public string TransactionId;
        public SIPTransactionStatesEnum TransactionState = SIPTransactionStatesEnum.Unknown;
        public SIPTransactionTypesEnum TransactionType;
        public DateTime Created = DateTime.Now;

        protected string m_branchId;
        public string BranchId
        {
            get { return m_branchId; }
        }

        protected string m_callId;
        protected string m_localTag;
        protected string m_remoteTag;

        protected SIPRequest m_initialRequest;
        public SIPRequest InitialRequest
        {
            get { return m_initialRequest; }
        }

        [DataMember]
        public SIPURI RequestURI
        {
            get { return m_initialRequest.URI; }
        }

        [DataMember]
        public SIPUserField RequestFrom
        {
            get { return m_initialRequest.Header.From.FromUserField; }
        }

        [DataMember]
        public IPEndPoint InitialRequestEndPoint;

        protected SIPResponse m_finalResponse;
        public SIPResponse FinalResponse
        {
            get { return m_finalResponse; }
        }

        [DataMember]
        public IPEndPoint RemoteEndPoint = null;        // This is used when the information in a SIP Request does not indicate the desired end point to send to. An example is a URI for a certain realm in a REGISTER request that does not match the SIP server address.

        public IPEndPoint SourceEndPoint = null;        // if this is set the Transport layer will attempt to send from.

        public event SIPTransactionStateChange TransactionStateChanged;
        public event SIPTransactionAuthenticationRequired TransactionAuthenticationRequired;
        public event SIPTransactionResponseReceived TransactionInformationResponseReceived;
        public event SIPTransactionResponseReceived TransactionFinalResponseReceived;
        public event SIPTransactionCancelReceived TransactionCancellationRequestReceived;
        public event SIPTransactionTimedOut TransactionTimedOut;
        public event SIPTransactionRequestRetransmit TransactionRequestRetransmit;
        public event SIPTransactionResponseRetransmit TransactionResponseRetransmit;

		private static Dictionary<string, SIPTransaction> m_transactions = new Dictionary<string, SIPTransaction>();

        public static int Count
        {
            get { return m_transactions.Count; }
        }

        public SIPTransaction(SIPRequest sipRequest)
        {
            try
            {
                RemoveExpiredTransactions();

                m_initialRequest = sipRequest;
                InitialRequestEndPoint = sipRequest.ReceivedFrom;

                TransactionId = GetRequestTransactionId(sipRequest);

                m_branchId = sipRequest.Header.Via.TopViaHeader.Branch;
                m_callId = sipRequest.Header.CallId;
                m_localTag = sipRequest.Header.From.FromTag;

                if (!m_transactions.ContainsKey(TransactionId))
                {
                    //logger.Debug("New transaction inserted for " + sipRequest.Method + " and " + TransactionId + ".");

                    lock (m_transactions)
                    {
                        m_transactions.Add(TransactionId, this);
                    }
                }
                else
                {
                    logger.Error("An attempt was made to add a duplicate SIP transaction.");

                    throw new ApplicationException("An attempt was made to add a duplicate SIP transaction.");
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception SIPTransaction (ctor). " + excp.Message);
                throw excp;
            }
        }

        public virtual void GotRequest(IPEndPoint localEndPoint, IPEndPoint inEndPoint, SIPRequest sipRequest)
        {}

        public virtual void GotResponse(IPEndPoint localEndPoint, IPEndPoint inEndPoint, SIPResponse sipResponse)
        {
            if (sipResponse.StatusCode >= 100 && sipResponse.StatusCode <= 199)
            {
                FireTransactionInformationResponseReceivedEvent(localEndPoint, inEndPoint, sipResponse);
            }
            else
            {
                FireTransactionFinalResponseReceivedEvent(localEndPoint, inEndPoint, sipResponse);
            }
        }

        public void Cancel()
        {
            if (TransactionCancellationRequestReceived != null)
            {
                TransactionCancellationRequestReceived(this);
            }
        }

        public static void AddTransaction(SIPRequest sipRequest)
        {
            SIPTransaction newTransaction = new SIPTransaction(sipRequest);
        }

        public static SIPTransaction GetTransaction(SIPRequest sipRequest)
        {
            if (sipRequest.Method != SIPMethodsEnum.ACK)
            {
                string transactionId = GetRequestTransactionId(sipRequest);

                if (m_transactions.ContainsKey(transactionId))
                {
                    return m_transactions[transactionId];
                }
                else
                {
                    return null;
                }
            }
            else
            {
                foreach (KeyValuePair<string, SIPTransaction> entry in m_transactions)
                {
                    SIPTransaction transaction = entry.Value;

                    if (transaction.m_callId == sipRequest.Header.CallId &&
                        transaction.m_localTag == sipRequest.Header.From.FromTag)
                    {
                        return transaction;
                    }
                }

                return null;
            }
        }

        public static SIPTransaction GetTransaction(SIPResponse sipResponse)
        {
           string transactionId = GetResponseTransactionId(sipResponse);

            if (m_transactions.ContainsKey(transactionId))
            {
                return m_transactions[transactionId];
            }
            else
            {
                return null;
            }
        }

        protected static SIPTransaction GetTransaction(string transactionId)
        {
            if (m_transactions.ContainsKey(transactionId))
            {
                return m_transactions[transactionId];
            }
            else
            {
                return null;
            }
        }

        public static bool IsCompletedTransaction(SIPRequest sipRequest)
        {
            string transactionId = GetRequestTransactionId(sipRequest);

            if (m_transactions.ContainsKey(transactionId))
            {
                if (sipRequest.Method != SIPMethodsEnum.ACK)
                {
                    return m_transactions[transactionId].TransactionState == SIPTransactionStatesEnum.Completed;
                }
                else
                {
                    return m_transactions[transactionId].TransactionState == SIPTransactionStatesEnum.Confirmed;
                }
            }
            else
            {
                return false;
            }
        }

        public static bool IsCompletedTransaction(SIPResponse sipResponse)
        {
            string transactionId = GetResponseTransactionId(sipResponse);

            if (m_transactions.ContainsKey(transactionId))
            {
                return m_transactions[transactionId].TransactionState == SIPTransactionStatesEnum.Completed;
            }
            else
            {
                return false;
            }
        }

        public static bool Exists(SIPRequest sipRequest)
        {
            return GetTransaction(sipRequest) != null;
        }

        public static bool Exists(SIPResponse sipResponse)
        {
            return GetTransaction(sipResponse) != null;
        }

        public void SendFinalResponse(SIPResponse finalResponse)
        {
            //logger.Debug("Transaction final response sent for " + m_initialRequest.Method + " as " + finalResponse.StatusCode + " " + finalResponse.ReasonPhrase + " and " + TransactionId + ".");
            
            m_finalResponse = finalResponse;
            TransactionState = SIPTransactionStatesEnum.Completed;

            if (TransactionType == SIPTransactionTypesEnum.Invite)
            {
                SIPTransport.SendSIPReliable(this);
            }
            else
            {
                SIPTransport.SendResponse(finalResponse.Header.Via.TopViaHeader.GetEndPoint(), finalResponse);
            }
        }

        public void SendInformationalResponse(SIPResponse sipResponse)
        {
            try
            {
                //logger.Debug("Information response being sent sent for " + m_initialRequest.Method + " as " + sipResponse.StatusCode + " " + sipResponse.ReasonPhrase + " and " + TransactionId + ".");

                SIPTransport.SendResponse(sipResponse.Header.Via.TopViaHeader.GetEndPoint(), sipResponse);
            }
            catch (Exception excp)
            {
                logger.Error("Exception SendInformationalResponse. " + excp.Message);
                throw excp;
            }
        }

        private static string GetRequestTransactionId(SIPRequest sipRequest)
        {
            string method = (sipRequest.Method == SIPMethodsEnum.ACK) ? SIPMethodsEnum.INVITE.ToString() : sipRequest.Method.ToString();

            return GetRequestTransactionId(method, sipRequest.Header.Via.TopViaHeader.Branch);
        }

        protected static string GetRequestTransactionId(string method, string branchId)
        {
            return Crypto.GetSHAHash(method + branchId);
        }

        private static string GetResponseTransactionId(SIPResponse sipResponse)
        {
            //string responseId = StatusCode + ReasonPhrase + Header.CSeqMethod + Header.CSeq + Header.Via.TopViaHeader + Header.From;
            //string responseId = sipResponse.Header.CSeqMethod + sipResponse.Header.Via.TopViaHeader.Branch + sipResponse.Header.Via.TopViaHeader.ReceivedFromIPAddress;
            string responseId = sipResponse.Header.CSeqMethod + sipResponse.Header.Via.TopViaHeader.Branch;

            return Crypto.GetSHAHash(responseId);
        }

        private static void RemoveExpiredTransactions()
        {
            try
            {
                lock (m_transactions)
                {
                    List<string> removals = new List<string>();
                    foreach (SIPTransaction transaction in m_transactions.Values)
                    {
                        if (DateTime.Now.Subtract(transaction.Created).TotalMilliseconds >= TRANSACTION_DURATION * m_t1)
                        {
                            removals.Add(transaction.TransactionId);
                        }
                    }

                    foreach (string transactionId in removals)
                    {
                        m_transactions.Remove(transactionId);
                    }
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception RemoveExpiredTransaction. " + excp.Message);
            }
        }

        public static void RemoveAll()
        {
            lock (m_transactions)
            {
                m_transactions.Clear();
            }
        }

        protected SIPResponse GetInformationalResponse(SIPHeader requestHeader, SIPResponseStatusCodesEnum sipResponseCode)
        {
            try
            {
                SIPResponse informationalResponse = new SIPResponse(sipResponseCode);

                informationalResponse.Header = new SIPHeader(requestHeader.From, requestHeader.To, requestHeader.CSeq, requestHeader.CallId);
                informationalResponse.Header.CSeqMethod = requestHeader.CSeqMethod;
                informationalResponse.Header.Via = requestHeader.Via;
                informationalResponse.Header.MaxForwards = Int32.MinValue;
                informationalResponse.Header.Timestamp = requestHeader.Timestamp;

                return informationalResponse;
            }
            catch (Exception excp)
            {
                logger.Error("Exception GetInformationalResponse. " + excp.Message);
                throw excp;
            }
        }

        public void TimedOut()
        {
            if (TransactionTimedOut != null)
            {
                try
                {
                    TransactionTimedOut(this);
                }
                catch (Exception excp)
                {
                    logger.Error("Exception TransactionTimedOut. " + excp.Message);
                }
            }
        }

        public void RequestRetransmit()
        {
            if (TransactionRequestRetransmit != null)
            {
                try
                {
                    TransactionRequestRetransmit(this, this.InitialRequest, this.Retransmits);
                }
                catch (Exception excp)
                {
                    logger.Error("Exception TransactionRequestRetransmit. " + excp.Message);
                }
            }
        }

        public void ResponseRetransmit()
        {
            if (TransactionResponseRetransmit != null)
            {
                try
                {
                    TransactionResponseRetransmit(this, this.FinalResponse, this.Retransmits);
                }
                catch (Exception excp)
                {
                    logger.Error("Exception TransactionResponseRetransmit. " + excp.Message);
                }
            }
        }

        protected void FireTransactionStateChangedEvent()
        {
            if (TransactionStateChanged != null)
            {
                try
                {
                    TransactionStateChanged(this);
                }
                catch (Exception excp)
                {
                    logger.Error("Exception FireTransactionStateChangedEvent. " + excp.Message);
                }
            }
        }

        protected void FireTransactionAuthenticationRequiredEvent(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, SIPAuthenticationHeader authenticationHeader)
        {
            if (TransactionAuthenticationRequired != null)
            {
                try
                {
                    TransactionAuthenticationRequired(localEndPoint, remoteEndPoint, this, authenticationHeader);
                }
                catch (Exception excp)
                {
                    logger.Error("Exception FireTransactionAuthetnicationRequiredEvent. " + excp.Message);
                }
            }
        }

        protected void FireTransactionInformationResponseReceivedEvent(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, SIPResponse sipResponse)
        {
            if (TransactionInformationResponseReceived != null)
            {
                try
                {
                    TransactionInformationResponseReceived(localEndPoint, remoteEndPoint, this, sipResponse);
                }
                catch (Exception excp)
                {
                    logger.Error("Exception FireTransactionInformationResponseReceivedEvent. " + excp.Message);
                }
            }
        }

        protected void FireTransactionFinalResponseReceivedEvent(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, SIPResponse sipResponse)
        {
            if (TransactionFinalResponseReceived != null)
            {
                try
                {
                    TransactionFinalResponseReceived(localEndPoint, remoteEndPoint, this, sipResponse);
                }
                catch (Exception excp)
                {
                    logger.Error("Exception FireTransactionFinalResponseReceivedEvent. " + excp.Message);
                }
            }
        }
    }
}
